Android: fix controller rumble motor separation via VibratorManager (API 31+)#18906
Merged
LibretroAdmin merged 3 commits intolibretro:masterfrom Apr 7, 2026
Merged
Android: fix controller rumble motor separation via VibratorManager (API 31+)#18906LibretroAdmin merged 3 commits intolibretro:masterfrom
LibretroAdmin merged 3 commits intolibretro:masterfrom
Conversation
…API 31+) RetroArch Android collapsed both libretro rumble channels (RETRO_RUMBLE_STRONG and RETRO_RUMBLE_WEAK) into a single vibration output by OR-merging their amplitudes before calling InputDevice.getVibrator(). This made the strong (large/low-frequency) and weak (small/high-frequency) motors feel identical — rendering motor separation in cores like SwanStation completely ineffective. Fixes libretro/swanstation#72. Changes ------- android_joypad.c: - Remove the OR-merge of strong | weak into new_strength. Each channel is now updated independently and held in the existing last_strength_strong / last_strength_weak per-port state. - Add a new JNI call path to doVibrateJoypad when the method is registered and id >= 0 (controller, not device vibration). Both normalized amplitudes are passed separately so the Java side can route them to individual motors. - Preserve the original OR-merge behavior as the legacy fallback for the device vibration path (id == -1) and for builds where doVibrateJoypad is unavailable. RetroActivityCommon.java: - Add doVibrateJoypad(int id, int strongStrength, int weakStrength, int unused), the new JNI entry point for per-channel controller rumble. - On Android 12+ (API 31), doVibrateJoypadApi31() uses InputDevice.getVibratorManager() to enumerate the controller's vibrator IDs and drives them independently via CombinedVibration.startParallel(). Index 0 is mapped to the strong (large/low-freq) motor and index 1 to the weak (small/high-freq) motor. This ordering is consistent across tested controllers but is not guaranteed by the Android API; the code degrades gracefully. - If the controller exposes only one vibrator, max(strong, weak) is used so the device still rumbles. - If VibratorManager returns no vibrator IDs, or on Android < 12, falls back to the existing doVibrate() single-vibrator path. - The existing doVibrate() method and the "Enable Device Vibration" path are completely unchanged. platform_unix.h / platform_unix.c: - Register doVibrateJoypad as a new jmethodID alongside doVibrate. GET_METHOD_ID clears exceptions on lookup failure, so doVibrateJoypad is NULL on older APKs and the C-side fallback fires automatically. build.gradle: - Bump compileSdkVersion from 30 to 31. Required to reference VibratorManager and CombinedVibration at compile time. minSdkVersion is unchanged (API 16). Tested ------ Confirmed strong and weak motors are now driven independently on Android 14+: - Sony DualShock 4 (Bluetooth) - Sony DualSense (Bluetooth) - 8BitDo Pro 2 in Xbox One mode (Bluetooth) Verified in SwanStation running Metal Gear Solid (PS1): weak-only rumble events (e.g. distant explosions) are now perceptibly lighter than strong-only events (e.g. alert state), which were previously indistinguishable. Fallback behavior confirmed unaffected: controllers on Android < 12 still rumble via the legacy single-vibrator path, and "Enable Device Vibration" continues to function as before.
…API 31+) RetroArch Android collapsed both libretro rumble channels (RETRO_RUMBLE_STRONG and RETRO_RUMBLE_WEAK) into a single vibration output by OR-merging their amplitudes before calling InputDevice.getVibrator(). The effect type was discarded at the JNI call site, making weak and strong rumble completely indistinguishable on dual-motor controllers. Introduces doVibrateJoypad, a new JNI method that accepts both channels separately. On Android 12+ (API 31) it uses InputDevice.getVibratorManager() to enumerate controller vibrator IDs and drives each motor independently via CombinedVibration.startParallel(). Index 0 maps to the strong (large/low-freq) motor, index 1 to the weak (small/high-freq) motor — consistent across tested controllers but not guaranteed by the Android API; documented as a best-effort heuristic. Falls back to the existing single-vibrator doVibrate() path on Android < 12, single-vibrator controllers, or if doVibrateJoypad is not found at JNI lookup time. The doVibrate() method and "Enable Device Vibration" path are entirely unchanged. compileSdkVersion bumped 30 -> 31 (minimum required for VibratorManager and CombinedVibration at compile time; minSdkVersion unchanged at API 16). No deprecated APIs introduced. C changes are C89 and ISO C++ compatible. Fixes libretro/swanstation#72. Tested on Android 14: - Sony DualShock 4 (Bluetooth and USB) - Sony DualSense (Bluetooth and USB) - 8BitDo Pro 2 in Xbox One mode (Bluetooth)
…into DualShock-Fix
Contributor
Author
|
Note! This only fixes the vibration in Bluetooth mode! Working on a USB fix too. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Android's joypad rumble backend has been collapsing both libretro motor channels into a single vibration output since its introduction.
android_input_set_rumble_internalOR-mergedRETRO_RUMBLE_STRONGandRETRO_RUMBLE_WEAKamplitudes before forwarding them toInputDevice.getVibrator()— a single-vibrator API with no concept of individual motors. Theeffecttype was discarded at the JNI call site, so every rumble event regardless of channel drove the same motor at the same merged strength. Weak and strong rumble were completely indistinguishable on any dual-motor controller.This patch introduces a new
doVibrateJoypadJNI method that accepts both channels separately and usesInputDevice.getVibratorManager()on Android 12+ (API 31) to drive each controller motor independently.Changes:
android_joypad.c: Remove the OR-merge ofstrong | weak. Each channel is updated independently in the existing per-port state arrays. WhendoVibrateJoypadis registered andid >= 0, both normalised amplitudes (0–255) are passed to Java separately. The device vibration path (id == -1) and the legacy OR-merge fallback are preserved exactly.RetroActivityCommon.java: AdddoVibrateJoypad(int id, int strong, int weak, int unused). On Android 12+,doVibrateJoypadApi31usesInputDevice.getVibratorManager()to enumerate vibrator IDs and drives them independently viaCombinedVibration.startParallel(). Index 0 → strong (large/low-freq), index 1 → weak (small/high-freq); consistent across all tested controllers but not guaranteed by the Android API and documented as such. If only one vibrator is exposed,max(strong, weak)is used. If zero vibrators are exposed or on Android < 12, returnsfalseanddoVibrate()is called as before.doVibrate()itself is untouched.platform_unix.h/platform_unix.c: RegisterdoVibrateJoypadalongsidedoVibrateviaGET_METHOD_ID. Clears exceptions on failure sodoVibrateJoypadis safelyNULLon any build without the new Java method, causing the C-side fallback to fire automatically.build.gradle:compileSdkVersionbumped 30 → 31, required to referenceVibratorManagerandCombinedVibrationat compile time.minSdkVersionunchanged at API 16.Compatibility:
doVibrate()single-vibrator behaviour.id == -1) is unaffected.C89_BUILD=1.-Wallwarnings introduced.Tested on Android 14:
Tested with SwanStation in Metal Gear Solid's options. Weak and strong rumbles now behave properly. They were previously indistinguishable.
Related Issues
libretro/swanstation#72 — open since May 18, 2023
Related Pull Requests
None.
Reviewers
Please check
git log --follow input/drivers_joypad/android_joypad.cfor recent contributors to this area of the codebase.